classdef spyview < handle
% class for manipulating and viewing 2D data.
%  constructor:  sv=spyview(ma,xa,ya)
%  typical usage:
%    s=spyview(rand(10));

   properties
     % Plot data
     m=1;     % Matrix containing color data
     mo=1;    % Matrix containing original color data.
     x=[0 1]; % Vector containing xrange, assign with either [start,stop], or [x1,x2,x3];
     y=[0 1]; % Vector containing xrange, assign with either [start,stop], or [x1,x2,x3];
     
     % Filters data
     filters; % Cell array of active filters
     
     % Limits data
     minz=0;      % Minimum z value
     maxz=1;      % Maximum z value
     wpcnt=0.01;  % white percentage.  Resets maxz.
     bpcnt=0.01;  % black percentage.  Resets minz
     use_wpcnt=1; % Update min based on wpcnt.
     use_bpcnt=1; % Update max based on wpcnt
     
     % Colormap/Colorscale data
     cmap='jet';       % This can either be a name (like 'jet'), or a nx3 array
     nancolor=[0 1 0]; % Color to use for data that is nan.
     gamma=1;          % This is applied as an implicit 'filter'. 
     gammacenter=0;
     
     % Other nice stuff
     title;                                          % Title of spyview plot.
     xlabel;                                         % Axis labels
     ylabel;                                         % 
     zlabel;                                        
     linecutl_style={'Color',[1 0 0],'LineWidth',2}; % Style of linecut line  
     
     % Optional handlers
     reload_handler;                                 % Function to call to reload data
    end
   
   properties (Transient = true)  % Don't manipulate these from outside spyview.
     pm;         % Matrix containing processed color data;
     pmi;        % Matrix containing indexed color data (colormap indices)  
     nanmask;    % Matrix of points that are nan.
     px;         % Processed x-vector (may be cropped or scaled)
     py;         % Processed y-vector (may be cropped or scaled)
     pms;        % Vector of sorted z-data from pm; used for histograms.
     cmapd;      % nx3 array of actual color map.
     down=0;     % 1 if mouse is dragging 
     dragstart;  % coordinates of where the drag started
     idragstart; % x,y index of where drag started.
     
     % Internal data
     f=[];             % Figure containing the plot data.
     cpanel=[];        % Handle to the gui control panel
     filter_panel=[];  % 
     img=[];           % Handle to 2D image data
     linecut;          % Linecut plot
     linecutl;         % Linecut line on main image
     ovl_axes=[];         % Axes used for text overlay
     ovl_cpos=[];
   end
   
   properties (Constant = true)
     builtInCmaps = {'hsv','hot','gray','bone','copper','pink','white','flag', ...
          'lines','colorcube','jet','prism','cool','autumn','spring', ...
          'winter','summer'};
   end
   
   methods
       % Initialize a new spyview data structure
       function sv=spyview(ma,xa,ya,autostart)
           if exist('ma','var');  sv.m=ma; end
           if exist('xa','var');  sv.x=xa; end
           if exist('ya','var');  sv.y=ya; end
           if nargin > 0  % Autostart if this is a new instance.
               if ~exist('autostart','var') || autostart 
                 sv.start();
               end
           end
           p=fileparts(which('spyview'));
           addpath([p filesep 'filters']);
       end

       % Cleanup spyview
       function delete(sv)
          if ishandle(sv.cpanel)
              close(sv.cpanel);
          end             
          if ishandle(sv.filter_panel)
              close(sv.filter_panel);
          end       
          fprintf('Bye from spyview!\n'); 
       end

       %% User Functions
       % Start the spyview gui/plotting
       function start(sv)
           sv.cmap=sv.cmap; % Trigger a colormap rebuild
           sv.m=sv.mo;       % Trigger a full recalculate cycle.
           sv.x=sv.x;
           sv.y=sv.y;
           sv.openFigure();
           sv.redraw();
       end
       
       % Open the spyview plot window
       function openFigure(sv)
           if isempty(sv.f) || ~ishandle(sv.f)
               sv.f=figure();
               if ~isempty(sv.title)
                   set(sv.f,'Name',tile);
               end
           end
           set(sv.f,'UserData',sv);
           set(sv.f,'WindowKeyPressFcn',@(h,e) sv.keyPressed(e));
           set(sv.f,'DeleteFcn',@(h,e) sv.delete);
           set(sv.f,'WindowButtonDownFcn',@(h,e) sv.mouseDown);
           set(sv.f,'WindowButtonUpFcn',@(h,e) sv.mouseUp);
           set(sv.f,'WindowButtonMotionFcn',@(h,e) sv.mouseFunction);
           
       end
       
       
       %% Callbacks
       function keyPressed(sv, eventData)
           switch(eventData.Key)
               case 'r'
                   if ~isempty(sv.reload_handler)
                       sv.reload_handler(sv);
                       return;
                   end
               case 'p'
                   if ~isempty(sv.cpanel) && ishandle(sv.cpanel)
                       close(sv.cpanel);
                       sv.cpanel=[];
                       return;
                   else
                       sv.cpanel=spyview_cpanel;
                       gd=guidata(sv.cpanel);
                       gd.sv = sv;
                       guidata(sv.cpanel, gd);
                       sv.updateCpanel(1);
                   end
               case 'f'
                   if ~isempty(sv.filter_panel) && ishandle(sv.filter_panel)
                       close(sv.filter_panel);
                       sv.filter_panel=[];
                       return;
                   else
                       sv.filter_panel=spyview_filterpanel;
                       gd=guidata(sv.filter_panel);
                       gd.sv = sv;
                       guidata(sv.filter_panel, gd);
                       sv.updateFilterPanel();
                   end
           end
       end
       
       function [cp, icp] = getMouse(sv)
           ah=get(sv.img,'Parent');
           cp=get(ah,'CurrentPoint');
           cp=cp(1,1:2);
           icp(1)=max(1,min(size(sv.pm,2), 1+round((size(sv.pm,2)-1)*(cp(1)-sv.x(1))/(sv.x(end)-sv.x(1)))));
           icp(2)=max(1,min(size(sv.pm,1), 1+round((size(sv.pm,1)-1)*(cp(2)-sv.y(1))/(sv.y(end)-sv.y(1)))));
       end
       
       function mouseDown(sv)
           sv.down=1;
           [sv.dragstart, sv.idragstart] = sv.getMouse;
           sv.mouseFunction();
       end
       
       function mouseUp(sv)
           sv.mouseFunction();
           sv.down=0;
       end
       
       function mouseFunction(sv)
           if ~isempty(sv.cpanel)
               [cp, icp] = sv.getMouse;
               g=guidata(sv.cpanel);               
               set(g.cpos,'String',sprintf('(%.4g,%.4g)=%.3g',cp,sv.pm(icp(2),icp(1))));
           end
           if ~sv.down               
               return;
           end
           if isempty(sv.cpanel)  % If the panel isn't open, we don't have the coordinates yet.
               [cp, icp] = sv.getMouse;
           end
           switch(get(sv.f,'SelectionType'))
               case 'normal'
                   x(1:2)=sv.px(icp(1));
                   y(1:2)=[sv.py(1) sv.py(end)];
                   lcx=sv.py;
                   lcy=sv.pm(:,icp(1));
                   sv.showLineCut(x,y,lcx,lcy);
               case 'alt'
                   y(1:2)=sv.py(icp(2));
                   x(1:2)=[sv.px(1) sv.px(end)];
                   lcx=sv.px;
                   lcy=sv.pm(icp(2),:);
                   sv.showLineCut(x,y,lcx,lcy);
               case 'extend' %middle click.drag
                   
               otherwise
                   fprintf('Unknown selection type: %s\n', get(sv.f,'SelectionType'));                   
           end
       end
         
       %% Set/get functions
       function set.wpcnt(sv,wpp)
           sv.wpcnt=wpp;
           sv.maxz=sv.pms(1+ceil((1-sv.wpcnt)*(length(sv.pms)-1)));
       end
       
       function set.bpcnt(sv,wpp)
           sv.bpcnt=wpp;
           sv.minz=sv.pms(1+floor(sv.bpcnt*(length(sv.pms)-1)));
       end
       
       function set.gamma(sv,s)
           if sv.gamma ~= s
               sv.gamma = s;
               sv.updateIndexed;
               sv.redraw;
               sv.updateCpanel();
           end
       end
       
       function set.gammacenter(sv,s)
           if sv.gammacenter ~= s
               sv.gammacenter = s;
               sv.updateIndexed;
               sv.redraw;
               sv.updateCpanel();
           end
       end
       
       function set.minz(sv,s)
           if sv.minz ~= s
               sv.minz=s;
               sv.updateIndexed;
               sv.redraw;
               sv.updateCpanel();
           end
       end
       
       function set.maxz(sv,s)
           if sv.maxz ~= s
               sv.maxz=s;
               sv.updateIndexed;
               sv.redraw;
               sv.updateCpanel();
           end
       end
      
       function set.m(sv, nm)
          sv.mo = nm;
          sv.m = nm;
          sv.nanmask=isnan(sv.m);
          sv.m(sv.nanmask)=nanmean(sv.m(:));
          sv.runFilters();
       end
       
       function set.filters(sv,val)
           sv.filters=val;           
           sv.runFilters();           
       end
           
       function set.x(sv,val)
           if length(val) == size(sv.m,2)
               sv.x=val;
           else
               sv.x=linspace(val(1),val(2),size(sv.m,2));
           end
           sv.runFilters();
       end
       
       function set.y(sv,val)
           if length(val) == size(sv.m,1)
               sv.y=val;
           else
               sv.y=linspace(val(1),val(2),size(sv.m,1));
           end
           sv.runFilters();
       end
       
       
       % Update the settings on the control panel. If cmaps is 1, also
       % update the colormaps.
       function updateCpanel(sv, cmaps)
          if nargin < 2
              cmaps=0;
          end
          if ~isempty(sv.cpanel) && ishandle(sv.cpanel)
            spyview_cpanel('update_cpanel',sv.cpanel,cmaps,[]);
          end
       end

       % Update the settings on the filter panel.
       function updateFilterPanel(sv)
          if ~isempty(sv.filter_panel) && ishandle(sv.filter_panel)
            spyview_filterpanel('update_panel',sv.filter_panel,[],[]);
          end
       end

       %% Colormap/imagedata calculations
       function updatePcnts(sv)
           if sv.use_bpcnt
              sv.minz=sv.pms(1+floor(sv.bpcnt*(length(sv.pms)-1)));    
           end
           if sv.use_wpcnt
               sv.maxz=sv.pms(1+ceil((1-sv.wpcnt)*(length(sv.pms)-1)));
           end
       end
       
       function runFilters(sv)
          sv.pm = sv.m; sv.px = sv.x; sv.py = sv.y;
          for i=1:length(sv.filters)
              if sv.filters{i}.enabled
                [sv.pm, sv.px, sv.py] = sv.filters{i}.filter(sv.pm,sv.px,sv.py);
              end
          end         
          sv.pms=sort(sv.pm(:));
          sv.updateIndexed;
          sv.redraw;
          sv.updateCpanel();
       end
       
       function f=listFilters(sv)  % Fixme -- This should figure out correct root dir.
          f=spyview_listfilt; 
       end
  
       %% Colormap manipulation routines
       function cmaps=listColormaps(sv)
         cmaps=sv.builtInCmaps;  
       end
      
       % Fetch a colormap by name
       function cmap = getCmap(sv, name)
           deflen=1024;
           if ~isempty(strmatch(name,sv.builtInCmaps,'exact'))
               cmf=str2func(name);
               cmap=cmf(deflen);
           else
               error('Unknown colormap ''%s''\n',cmap);
           end
       end
       
       function set.xlabel(sv,val)
           sv.xlabel=val;
           sv.redraw;
       end

       function set.ylabel(sv,val)
           sv.ylabel=val;
           sv.redraw;
       end

       
       function set.title(sv,val)
           sv.title=val;
           sv.redraw;
       end

       function set.cmap(sv, val)
           ol=size(sv.cmapd);
           if isstr(val)
               sv.cmapd=sv.getCmap(val);
           else
               sv.cmapd=val;
           end
           if ol ~= size(sv.cmapd)
               sv.updateIndexed;
           end
           sv.redraw; 
       end
       
       %% Drawing routines
       % Update the indexed copy of the image
       function updateIndexed(sv)
           sv.updatePcnts();
           s=length(sv.cmapd);
           x=min(max(0,(sv.pm - sv.minz)/(sv.maxz-sv.minz)),1);
           if sv.gamma ~= 1
               if sv.gammacenter ~= 0
                   gc=sv.gammacenter;
                   g=sv.gamma;
                   x = gc + (x <= gc) .* (-gc * ((gc-x)/(gc)).^g) + ...
                       (x > gc) .* ((1-gc) * ((x-gc)./(1-gc)).^g);
               else
                   x = x.^sv.gamma;
               end
           end

           sv.pmi=max(1,min(s,round(s*x)));
       
       end
       
       % Redraw the image
       function redraw(sv)
           if isempty(sv.f) || ~ishandle(sv.f)
               return;
           end
           
           if isempty(sv.img) || ~ishandle(sv.img)
               set(0,'CurrentFigure',sv.f)               
               sv.img=image(sv.x,sv.y,zeros([size(sv.pmi),3]));
               % Neat trick for packing figure tighter.
               ti=get(gca,'TightInset');
               li=get(gca,'LooseInset');
               set(gca,'LooseInset',[ti(1) li(2) ti(3) ti(4)]);
               axis xy;
           end
                          
           r=sv.cmapd(:,1);
           g=sv.cmapd(:,2);
           b=sv.cmapd(:,3);
           cdata(:,:,1)=r(sv.pmi);
           cdata(:,:,2)=g(sv.pmi);
           cdata(:,:,3)=b(sv.pmi);
           set(sv.img,'CData',cdata);
           
           ax=get(sv.f,'CurrentAxes');
           if ~strcmp(sv.xlabel,get(get(ax,'XLabel'),'String'))
             set(get(ax,'XLabel'),'String',sv.xlabel);
           end
           if ~strcmp(sv.ylabel,get(get(ax,'YLabel'),'String'))
             set(get(ax,'YLabel'),'String',sv.ylabel);
           end
           if ~strcmp(get(sv.f,'Name'),sv.title) && ~isempty(sv.title)
               set(sv.f,'Name',sv.title);
           end
       end
       
       % Draw the linecut
       function showLineCut(sv,x,y,lcx,lcy)
          % Draw the plot
           if isempty(sv.linecut) || ~ishandle(sv.linecut)
               s=get(sv.f,'Position');
               
               %Creating a new figure can trigger mouse callbacks.
               sv.down=0;
               f2=figure('Name','Spyview Linecut','Position',[s(1)+s(3), s(2), s(3), s(4)]);
               sv.linecut=plot(lcx,lcy);
           else
               set(sv.linecut,{'XData','YData'},{lcx,lcy});
           end
           
           % Draw the line on the main figure
           if isempty(sv.linecutl) || ~ishandle(sv.linecutl)
               axes(get(sv.img,'Parent'));
               sv.linecutl=line(x,y);
               set(sv.linecutl,sv.linecutl_style{:});
           else
               set(sv.linecutl,{'XData','YData'},{x,y});
           end
           
           % Update the axis labels.
           ax=get(sv.linecut,'Parent');
           if(diff(x) > diff(y))
               xl=sv.xlabel;
               ts=sprintf('%s=%.4g',sv.xlabel,mean(x));
           else
               xl=sv.ylabel;
               ts=sprintf('%s=%.4g',sv.ylabel,mean(y));
           end           
           if ~strcmp(xl,get(get(ax,'XLabel'),'String'))
             set(get(ax,'XLabel'),'String',xl);
           end
           if ~strcmp(ts,get(get(ax,'Title'),'String'))
             set(get(ax,'Title'),'String',ts);
           end
           if ~strcmp(sv.zlabel,get(get(ax,'YLabel'),'String'))
             set(get(ax,'YLabel'),'String',sv.zlabel);
           end
               
           
       end
   end
end